#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import os

from common.common import check_command_injection, check_path_legal
from common.const import SubJobStatusEnum, ReportDBLabel, RestoreTypeEnum, EnvInfoPrefix
from common.parse_parafile import get_env_variable
from sqlserver import log
from sqlserver.commons.common import output_action_result, check_is_sub_job_database, get_restore_channel, \
    is_new_restore_path
from sqlserver.commons.const import VDIActionType, SQLServerProgressFileType, ParamConstant, \
    SQLServerCode, BodyErr, ProgressConst, SqlServerReportLabel, SQLServerStrConstant, ExecCmdResult
from sqlserver.commons.sqlserver_utils import get_format_restore_time
from sqlserver.sqlserver_restore_base import SQLServerRestoreBase


class SQLServerRestoreDatabase(SQLServerRestoreBase):
    def __init__(self, p_id, job_id, sub_job_id, json_param):
        super().__init__(p_id, job_id, sub_job_id, json_param)
        self._job_id = job_id
        self.database_name = ""
        self.new_database_name = ""
        self.instance_name = ""
        self.auth_mode = ""
        self.user_name = ""
        self.pass_prefix = ""

    def get_restore_databases(self):
        # 获取待恢复的数据库名、重命名
        self.database_name = self._json_param.get("job", {}).get("targetObject", {}).get("name", "")
        self.new_database_name = self._json_param.get("job", {}).get("extendInfo", {}).get("newDatabaseName", "")

    def prepare_user_info(self):
        self.instance_name = self._json_param.get("job", {}).get("targetEnv", {}).get("nodes", [{}])[0]. \
            get("extendInfo", {}).get("instance", "")
        is_new_restore = is_new_restore_path(self._json_param)
        if is_new_restore:
            self.auth_mode = get_env_variable(f'{EnvInfoPrefix.NODE_RESTORE_AUTH_TYPE}_{self._p_id}')
            self.user_name = get_env_variable(f"{EnvInfoPrefix.NODE_RESTORE_USERNAME}_{self._p_id}")
            self.pass_prefix = f"{EnvInfoPrefix.NODE_RESTORE_PASS}_{self._p_id}"
        else:
            self.auth_mode = get_env_variable(f"{EnvInfoPrefix.RESTORE_AUTH_TYPE}_{self._p_id}")
            self.user_name = get_env_variable(f"{EnvInfoPrefix.RESTORE_USERNAME}_{self._p_id}")
            self.pass_prefix = f"{EnvInfoPrefix.RESTORE_PASS}_{self._p_id}"
        self.check_and_prepare_user_info(self.instance_name)
        self.get_restore_new_path()

    def allow_restore_in_local_node(self):
        log.info(f"Start to exec allow in local. pid: {self._p_id} jobId: {self._job_id}")
        if check_is_sub_job_database(self._json_param):
            log.info(f"Allow sub job. pid: {self._p_id} jobId: {self._job_id}")
            return True
        self.prepare_user_info()
        # 检查实例是否运行
        instance_name = self._json_param.get("job", {}).get("targetEnv", {}).get("nodes", [{}])[0]. \
            get("extendInfo", {}).get("instance", "")
        if check_command_injection(instance_name):
            log.error(f"The param {instance_name} has special characters.")
            return False
        if not self.sql_utils.check_service_is_running(instance_name):
            node_ip = self._json_param.get("job", {}).get("targetEnv", {}).get("nodes", [{}])[0].get("endpoint", "")
            output_action_result(self._p_id, SQLServerCode.FAILED.value,
                                 BodyErr.INSTANCE_STATE_ABNORMAL.value, "", [node_ip, instance_name])
            log.error(f"The instance: {instance_name} is not running")
            return False
        # 检查版本是否一致
        copy_info = self.get_extend_info()
        if not self.sql_utils.check_version_is_matched(copy_info.get("version", "")):
            output_action_result(self._p_id, SQLServerCode.FAILED.value,
                                 BodyErr.RESTORE_FAIL_FOR_VERSION_INCONSISTENCY.value, "")
            log.error(f"The version of SQL server is different, can not be restored.")
            return False
        return True

    def restore_pre_job(self):
        self.write_progress_to_file(SubJobStatusEnum.RUNNING.value, ProgressConst.MIN_PROGRESS.value,
                                    "", SQLServerProgressFileType.COMMON)
        self.prepare_user_info()
        # 检查用户权限
        if not self.sql_utils.check_user_permission(self.auth_mode, self.user_name):
            self.restore_fail_tail.log_detail = BodyErr.SQLSERVER_PERMISSIONS_ERROR.value
            log.error("Check user permission failed")
            raise Exception("Check user permission failed")
        self.get_restore_databases()
        # 非默认实例ReportServer和ReportServerTempDB名称不一样
        if self.instance_name.upper() == SQLServerStrConstant.SQLSERVER_DEFAULT_SERVICES.upper():
            report_list = ("ReportServer", "ReportServerTempDB")
        else:
            report_list = (f"ReportServer${self.instance_name}", f"ReportServer${self.instance_name}TempDB")
        # 系统数据库不支持单数据库恢复
        if self.database_name in SQLServerStrConstant.SYS_DATABASE or self.database_name in report_list:
            log.info(f"The system database: {self.database_name}，cannot be restored")
            raise Exception("The system database not support")
        # 新位置恢复检查同名数据库，数据库文件是否已存在
        is_new_restore = is_new_restore_path(self._json_param)
        if is_new_restore:
            # 新位置恢复使用副本中的名称
            self.database_name = self.database_name = self._json_param.get("job", {}).get("copies", [{}])[0]. \
                get("protectObject", {}).get("name", "")
            # 检查名称是否已存在
            check_name = self.database_name
            if self.new_database_name:
                check_name = self.new_database_name
            if self.sql_utils.check_db_is_exist([check_name]):
                self.restore_fail_tail.log_detail = BodyErr.RESTORE_FAIL_FOR_DATABASE_EXIST.value
                log.error(f"Database {check_name} with the same name exists.")
                raise Exception("Database exist")
            self.get_copy_info()
            return
        # 原位置恢复检查数据库是否使用中和新名称是否重复
        dt_list = self.sql_utils.check_database_in_use([self.database_name])
        if dt_list:
            self.restore_fail_tail.log_detail = BodyErr.SQLSERVER_DATABASE_IN_USE_ERROR.value
            self.restore_fail_tail.log_detail_param = [self.database_name]
            log.error(f"The database: {self.database_name} is in use")
            raise Exception("The database is in use")
        # 原位置需要恢复名称，增加名称检查
        restore_name = self._json_param.get("job", {}).get("copies", [{}])[0]. \
            get("protectObject", {}).get("name", "")
        if self.new_database_name:
            restore_name = self.new_database_name
        if self.database_name != restore_name and self.sql_utils.check_db_is_exist([restore_name]):
            self.restore_fail_tail.log_detail = BodyErr.RESTORE_FAIL_FOR_DATABASE_EXIST.value
            log.error(f"Database {self.new_database_name} with the same name exists.")
            raise Exception("Database exist")

    def exec_restore_pre_job(self):
        self.restore_fail_tail.log_info = ReportDBLabel.PRE_REQUISIT_FAILED
        try:
            self.restore_pre_job()
        except Exception as exception_str:
            self.write_progress_to_file(SubJobStatusEnum.FAILED.value, ProgressConst.PROGRESS_ONE_HUNDRED,
                                        self.restore_fail_tail, SQLServerProgressFileType.COMMON)
            log.error(f"Exception when pre job,{exception_str}")
            return False
        self.write_progress_to_file(SubJobStatusEnum.COMPLETED.value,
                                    ProgressConst.PROGRESS_ONE_HUNDRED, "", SQLServerProgressFileType.COMMON)
        return True

    def do_restore(self, database, vdi_type, data_path, meta_path, recovery_time: str = ""):
        log.info(f"Start restore database: {database}, vdi type: {vdi_type}")
        # 数据库可能重命名，不能直接用参数中的数据库名称
        file_list = os.listdir(data_path)
        copy_file_name = ""
        for file in file_list:
            if file.endswith(".bak"):
                copy_file_name = file.split('.bak')[0]
                break
        # VDI工具不支持realpath
        path = os.path.join(data_path, f"{copy_file_name}.bak")
        if not check_path_legal(path, data_path):
            log.error(f"The param {path} is not real path.")
            raise Exception(f"The param {path} is not real path.")
        if not os.path.exists(path):
            log.error(f"Copy path: {path} not exist")
            raise Exception(f"Copy path: {path} not exist")
        if self.new_database_name:
            restore_info = self.get_database_restore_info(copy_file_name,
                                                          self.new_restore_path, meta_path, self.new_database_name)
        else:
            restore_info = self.get_database_restore_info(copy_file_name,
                                                          self.new_restore_path, meta_path, self.database_name)
        # 获取原数据库名称
        origin_database = self._json_param.get("job", {}).get("targetObject", {}).get("name", "")
        channel = get_restore_channel(data_path, origin_database)
        cmd = f"{ParamConstant.VDI_TOOL_PATH} {vdi_type} \"{database}\" \"{path}\" \"{self.vdi_info}\" " \
              f"\"{recovery_time}\" \"{restore_info}\" {channel}"
        log.info(f"cmd:{cmd}")
        code, std_out, std_err = self.sql_utils.get_command_result(cmd)
        if code != ExecCmdResult.SUCCESS:
            log.error(f"Failed to restore database: {database}, vdi type: {vdi_type}, out: {std_out}, err: {std_err}")
            raise Exception(f"Failed to restore database")
        log.info(f"End to restore database: {database}, vdi type: {vdi_type}")

    def log_restore(self, database_name):
        restore_time = self._json_param.get("job", {}).get("extendInfo", {}).get("restoreTimestamp", "")
        if restore_time:
            copies = self._json_param.get("job", {}).get("copies", [])
            restore_time = get_format_restore_time(restore_time, copies)

        # 全量副本恢复
        self.do_restore(database_name, VDIActionType.DIFF_RESTORE_WITH_NO_RECOVERY_REPLACE,
                        self.full_copy.get("dataPath"), self.full_copy.get("metaPath", ""))
        # 差异副本恢复
        if self.diff_copy:
            self.do_restore(database_name, VDIActionType.LOG_RESTORE_WITH_NO_RECOVERY,
                            self.diff_copy.get("dataPath"), self.diff_copy.get("metaPath", ""))
        length = len(self.log_copy.get("dataPath")) - 1
        for k, copy in enumerate(self.log_copy.get("dataPath")):
            meta_path = ""
            # 新位置位置恢复，获取meta路径
            if self.log_copy.get("metaPath"):
                meta_path = self.log_copy.get("metaPath")[k]
            self.do_restore(database_name, VDIActionType.POINT_IN_TIME_RESTORE_WITH_NO_RECOVERY, copy,
                            meta_path, restore_time)
            if k == length:
                self.do_restore(database_name, VDIActionType.POINT_IN_TIME_RESTORE_WITH_RECOVERY, copy,
                                meta_path, restore_time)
        return True, ""

    def restore_user_dbs(self, database_name):
        # restore other databases
        if self.restore_type == RestoreTypeEnum.FULL_RESTORE.value:
            self.do_restore(database_name, VDIActionType.FULL_RESTORE_WITH_REPLACE,
                            self.full_copy.get("dataPath"), self.full_copy.get("metaPath", ""))
        elif self.restore_type == RestoreTypeEnum.DIFF_RESTORE.value:
            # 全量恢复
            self.do_restore(database_name, VDIActionType.DIFF_RESTORE_WITH_NO_RECOVERY_REPLACE,
                            self.full_copy.get("dataPath"), self.full_copy.get("metaPath", ""))
            # 差异恢复
            self.do_restore(database_name, VDIActionType.DIFF_RESTORE_WITH_RECOVERY,
                            self.diff_copy.get("dataPath"), self.diff_copy.get("metaPath", ""))
        elif self.restore_type == RestoreTypeEnum.LOG_RESTORE.value:
            ret, msg = self.log_restore(database_name)
            if not ret:
                log.error(f"Failed to restore database: {database_name}")
                return False, msg
        else:
            log.error(f"Restore type not support. pid: {self._p_id} jobId: {self._job_id}")
            return False, ""
        log.info(f"Succeed to restore database: {database_name} pid: {self._p_id} jobId: {self._job_id}")
        return True, ""

    def format_meta_path_null(self):
        if self.full_copy.get("metaPath"):
            self.full_copy["metaPath"] = ""
        if self.diff_copy.get("metaPath"):
            self.diff_copy["metaPath"] = ""
        if self.log_copy.get("metaPath"):
            self.log_copy["metaPath"] = list()
        return True

    def rename_database(self, old_name, new_name):
        cmd = f"sqlcmd {self.user_info} -Q \"EXEC sp_renamedb \'{old_name}\',\'{new_name}\';\""
        ret, std_out, str_err = self.sql_utils.get_command_result(cmd)
        if not ret:
            log.error(f"Failed to rename database: {old_name} to new name: {new_name}")
            return False
        return True

    def do_sub_job(self):
        # 原位置恢复不配置restore_info
        if not self.format_meta_path_null():
            log.error(f"Format meta path failed! pid: {self._p_id} jobId: {self._job_id}")
            return False, ""
        # 原位置恢复，先恢复再充命名
        ret, msg = self.restore_user_dbs(self.database_name)
        if not ret:
            log.error(f"Exec restore failed! pid: {self._p_id} jobId: {self._job_id}")
            return False, msg
        # 数据库名称与副本中名称保持一致
        restore_name = self._json_param.get("job", {}).get("copies", [{}])[0]. \
            get("protectObject", {}).get("name", "")
        if self.new_database_name:
            restore_name = self.new_database_name
        if self.database_name != restore_name:
            if not self.rename_database(self.database_name, restore_name):
                log.error(f"Rename database name failed! pid: {self._p_id} jobId: {self._job_id}")
                return False, ""
        return True, ""

    def do_sub_job_new_address(self):
        # 新位置恢复使用新名称恢复
        self.database_name = self.database_name = self._json_param.get("job", {}).get("copies", [{}])[0]. \
            get("protectObject", {}).get("name", "")
        database_name = self.database_name
        if self.new_database_name:
            database_name = self.new_database_name
        return self.restore_user_dbs(database_name)

    def real_start_exec_sub_job(self):
        log.info(f"Begin to restore database. pid:{self._p_id} jobId:{self._job_id} sub_jobId:{self._sub_job_id}")
        self.prepare_user_info()
        self.get_restore_databases()
        self.get_copy_info()
        # 恢复中，"1"：待恢复，"0"：已恢复
        self.running_tail.log_info = SqlServerReportLabel.RESTORE_SUB_JOB_RUNNING_LABEL
        self.running_tail.log_info_param = [self._sub_job_id, "1", "0"]
        self.write_progress_to_file(SubJobStatusEnum.RUNNING.value, ProgressConst.MIN_PROGRESS,
                                    self.running_tail, self._sub_job_id)
        if not self.new_restore_path:
            ret, msg = self.do_sub_job()
        else:
            ret, msg = self.do_sub_job_new_address()
        if not ret:
            self.write_progress_to_file(SubJobStatusEnum.FAILED.value, ProgressConst.PROGRESS_ONE_HUNDRED,
                                        self.restore_fail_tail, self._sub_job_id)
            log.error(f"Do sub job failed! pid:{self._p_id} jobId:{self._job_id}")
            return False
        # 恢复成功，"1"：成功个数
        self.restore_success_tail.log_info_param = [self._sub_job_id, "1"]
        self.write_progress_to_file(SubJobStatusEnum.COMPLETED.value, ProgressConst.PROGRESS_ONE_HUNDRED,
                                    self.restore_success_tail, self._sub_job_id)
        log.info(f"Succeed to Restore. pid:{self._p_id} jobId:{self._job_id} sub_jobId:{self._sub_job_id}")
        return True

    def exec_sub_job(self):
        try:
            ret = self.real_start_exec_sub_job()
        except Exception as e_info:
            self.write_progress_to_file(SubJobStatusEnum.FAILED.value, ProgressConst.PROGRESS_ONE_HUNDRED,
                                        self.restore_fail_tail, self._sub_job_id)
            log.error(f"Exception when backup database as err: {e_info}. pid: {self._p_id} jobId: {self._job_id}")
            return False
        log.info(f"Sub job result: {ret}. pid: {self._p_id} jobId: {self._job_id}")
        return ret

    def post_job(self):
        # 非新位置恢复直接返回
        is_new_restore = is_new_restore_path(self._json_param)
        if not self.new_restore_path:
            log.info("Not new restore")
            return
        # 恢复成功直接返回
        restore_result = self._json_param.get("restoreJobResult", 1)
        if restore_result == 0:
            log.info("Restore succeed, no need to clear resource")
            return
        log.info("Restore failed, start to clear database")
        self.prepare_user_info()
        self.get_restore_databases()
        # 检查数据库是否已存在，存在则删除
        check_name = self.database_name
        if self.new_database_name:
            check_name = self.new_database_name
        if self.sql_utils.check_db_is_exist(check_name):
            log.info(f"Database exist and start to drop")
            self.sql_utils.drop_database(check_name)

    def exec_restore_post(self):
        self.write_progress_to_file(SubJobStatusEnum.RUNNING.value, ProgressConst.MIN_PROGRESS,
                                    "", self._sub_job_id)
        try:
            self.post_job()
        except Exception:
            log.exception("Exception when post job")
            self.write_progress_to_file(SubJobStatusEnum.FAILED.value, ProgressConst.PROGRESS_ONE_HUNDRED,
                                        "", self._sub_job_id)
            return False
        self.write_progress_to_file(SubJobStatusEnum.COMPLETED.value, ProgressConst.PROGRESS_ONE_HUNDRED,
                                    "", self._sub_job_id)
        return True

    def report_restore_progress(self):
        return self.report_progress_comm(self._sub_job_id)
